﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace Silverstone.DI
{
    /// <summary>
    ///     Used to register and resolve dependencies at runtime
    /// </summary>
    public class Container : IContainer
    {
        #region Fields

        private readonly Dictionary<Type, Type> registeredTypes = new Dictionary<Type, Type>();
        private readonly Dictionary<Type, ICreationStrategy> creationStrategies = new Dictionary<Type, ICreationStrategy>();
        private readonly Dictionary<Type, object> registeredInstances = new Dictionary<Type, object>();

        #endregion

        #region Constructor

        public Container()
        {
            this.RegisterInstance<IContainer>(this);
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Registers the specified concrete type against itself for creation.
        /// </summary>
        /// <typeparam name="TConcrete">The concrete type to register.</typeparam>
        public void RegisterSelf<TConcrete>()
        {
            RegisterForCreation<TConcrete, TConcrete>();
        }

        /// <summary>
        ///     Registers the specified concrete type against the specified abstract type for creation, using the
        ///     default creation strategy.
        /// </summary>
        /// <typeparam name="TConcrete">The concrete type to register.</typeparam>
        /// <typeparam name="TAbstract">The abstract type to register.</typeparam>
        public void RegisterForCreation<TConcrete, TAbstract>()
        {
            RegisterForCreation<TConcrete, TAbstract>(new DefaultCreationStrategy());
        }


        /// <summary>
        ///     Registers the specified concrete type against the specified abstract type for creation, using the
        ///     specified creation strategy.
        /// </summary>
        /// <typeparam name="TConcrete">The concrete type to register</typeparam>
        /// <typeparam name="TAbstract">The abstract type to register</typeparam>
        /// <param name="creationStrategy">The creation strategy to use when creating the instance.</param>
        public void RegisterForCreation<TConcrete, TAbstract>(ICreationStrategy creationStrategy)
        {
            Type key = typeof(TAbstract);
            if (registeredTypes.ContainsKey(key) ||
                registeredInstances.ContainsKey(key))
            {
                throw new Exception("Type " + key + " is already registered");
            }

            registeredTypes[key] = typeof(TConcrete);
            creationStrategies[key] = creationStrategy;
        }

        /// <summary>
        ///     Registers the specified instance against the specified abstract type to be used every time
        ///     the type is requested.
        /// </summary>
        /// <typeparam name="TAbstract">The abstract type to register.</typeparam>
        /// <param name="instance">The instance which implements/inherits the abstract type.</param>
        public void RegisterInstance<TAbstract>(TAbstract instance)
        {
            Type key = typeof(TAbstract);
            if (registeredTypes.ContainsKey(key) ||
                registeredInstances.ContainsKey(key))
            {
                throw new Exception("Type " + key + " is already registered");
            }

            registeredInstances[key] = instance;
        }

        /// <summary>
        ///     Gets the concrete implementation of the specified abstract type.
        /// </summary>
        /// <typeparam name="TAbstract">The abstract type to register</typeparam>
        /// <returns>The resolved instance, or null if not found</returns>
        public TAbstract Get<TAbstract>()
        {
            object instance = Get(typeof(TAbstract));

            if (instance == null)
            {
                throw new Exception("Type " + typeof(TAbstract) + " is not registered");
            }

            return (TAbstract) instance;
        }

        private object Get(Type type)
        {
            if (registeredInstances.ContainsKey(type))
            {
                return registeredInstances[type];
            }

            if (registeredTypes.ContainsKey(type))
            {
                Type concreteType = registeredTypes[type];
                ICreationStrategy creationStrategy = creationStrategies[type];

                object instance = creationStrategy.CreateInstance(concreteType, ResolveConstructorParams(concreteType));

                ResolveDependencies(instance, concreteType);

                return instance;
            }

            return null;
        }

        private void ResolveDependencies(object objectToResolve, Type objectType)
        {
            PropertyInfo[] properties = objectType.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                object value = Get(property.PropertyType);
                if (value != null)
                {
                    property.SetValue(objectToResolve, value, null);
                    ResolveDependencies(value, value.GetType());
                }
            }
        }

        private object[] ResolveConstructorParams(Type type)
        {
            ConstructorInfo constructor = GetConstructorToUse(type);

            ParameterInfo[] parameters = constructor.GetParameters();
            List<object> constructorParams = new List<object>(parameters.Length);
            foreach (ParameterInfo parameter in parameters)
            {
                object constructorParam = Get(parameter.ParameterType);

                if (constructorParam == null)
                {
                    throw new Exception("Constructor parameter " + parameter.Name + " on type " + type + " could not be resolved");
                }
                ResolveDependencies(constructorParam, parameter.ParameterType);
                constructorParams.Add(constructorParam);
            }

            return constructorParams.ToArray();
        }

        private ConstructorInfo GetConstructorToUse(Type type)
        {
            ConstructorInfo[] constructors = type.GetConstructors();

            if (constructors.Length == 0)
            {
                throw new Exception("No constructor found to instantiate type " + type);
            }

            ConstructorInfo constructorToUse = null;
            bool foundAttribute = false;
            foreach (ConstructorInfo c in constructors)
            {
                // Always use the constructor with the [Inject] attribute if one is found
                if (c.GetCustomAttributes(typeof(InjectionConstructorAttribute), false).Length > 0)
                {
                    if (foundAttribute)
                    {
                        throw new Exception("Only one constructor can be marked with the [InjectConstructor] attribute " + type);
                    }
                    constructorToUse = c;
                    foundAttribute = true;
                }

                if (!foundAttribute)
                {
                    // Default to the constructor with the most parameters
                    if (constructorToUse == null 
                        || constructorToUse.GetParameters().Length < c.GetParameters().Length)
                    {
                        constructorToUse = c;
                    }
                }
            }

            return constructorToUse;
        }

        #endregion
    }
}
